Thinking In React
検索可能なデータテーブルをReactを使って作ってみる
Start With A Mock
APIはすでに出来ていて、その戻り値は以下のようなもの
code:JSON
[
{category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
{category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
{category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
{category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
{category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
{category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];
デザイナが作ったモックは以下のようなもの
https://gyazo.com/bcc53a92d48d4abf788e11d214065783
Step 1: Break The UI Into A Component Hierarchy
まずはコンポーネントとサブコンポーネントをボックスでくくり、すべてに名前をつける
コンポーネントは単一責任の原則で分割する
データモデルの1つのピースを表す単位にコンポーネントを分割する
JSONの名前付けやUI部品の名前付けがヒントになる
https://gyazo.com/c8509b5d84d728ed420795fa61be3eaf
table:Component
FilterableProductTable orange
SearchBar blue
ProductTable green
ProductCategoryRow turquoise
ProductRow red
ProductTableコンポーネントのヘッダーをコンポーネント化しない
ただし、テーブルにソート機能を追加する場合など複雑な機能を持たせるならコンポーネント化する意味がある
コンポーネントの階層構造
https://gyazo.com/e27334fb5b58cc802a77ae8e97573bdc
Step 2: Build A Static Version in React
大きなプロジェクトの場合はトップダウンで大きなコンポーネントから作っていくほうがラク
これは要求から設計に落とし込んでいく過程で大きなコンポーネントを分解していくほうが費用対効果が高いということだろうか?
テスト駆動などの場合はボトムアップで小さなコンポーネントから作っていくほうがラク
こっちは小さく始める場合(MVPを作るとか)に当てはまるのかもしれない
ゴールとなる機能一覧が決まっておらず、とりあえずプロトタイプ作って世に出すぞ的な勢いが必要な場合か
A Brief Interlude: Props vs State
Step 3: Identify The Minimal (but complete) Representation Of UI State
インタラクティブなUIを作るにはstateを利用する
ここでのキーは「DRY(Don't Repeat Yourself)原則」である
例えば、TODOリストアプリで、TODOの件数をstateには持たず、計算して求めることがあげられる
stateとなるポイント
1. 親からpropsとして与えられるか?与えられる場合はstateではない
2. 変更されうるか?変更されない場合はstateではない
3. コンポーネントの他のpropsやstateから計算できるか?計算できる場合はstateではない
Step 4: Identify Where Your State Should Live
stateの置き場所は以下のようにして見つける
1. stateに依存してレンダリングされるコンポーネントを特定する
2. 見つけたコンポーネントの共通の親となるコンポーネントを見つける
3. 共通の親コンポーネントもしくは階層の上位のコンポーネントがstateを持つ
4. もし見つからない場合は、新たにstateを持つだけのコンポーネントを作り、それを階層の上位に配置する
Step 5: Add Inverse Data Flow
Formコンポーネントはデータフローが逆になる
これは親コンポーネントが子コンポーネントにprops経由でコールバック関数を渡し、子コンポーネントはユーザーによる入力があった際にそのコールバック関数を呼ぶように実装する
親コンポーネントから渡されたコールバック関数は、中でsetStateメソッドを呼び、アプリケーションが再レンダリングされる
code:JavaScript
class ProductCategoryRow extends React.Component {
render() {
const category = this.props.category;
return (
<tr>
<th colSpan="2">{category}</th>
</tr>
);
}
}
class ProductRow extends React.Component {
render() {
const product = this.props.product;
const name = product.stocked ? (
product.name
) : (
<span style={{ color: "red" }}>{product.name}</span>
);
return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>
);
}
}
class ProductTable extends React.Component {
render() {
const filterText = this.props.filterText;
const inStockOnly = this.props.inStockOnly;
const rows = [];
let lastCategory = null;
this.props.products.forEach(product => {
if (product.name.indexOf(filterText) === -1) {
return;
}
if (inStockOnly && !product.stocked) {
return;
}
if (product.category !== lastCategory) {
rows.push(
<ProductCategoryRow
category={product.category}
key={product.category}
/>
);
}
rows.push(<ProductRow product={product} key={product.name} />);
lastCategory = product.category;
});
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
}
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
this.handleInStockChange = this.handleInStockChange.bind(this);
}
handleFilterTextChange(e) {
this.props.onFilterTextChange(e.target.value);
}
handleInStockChange(e) {
this.props.onInStockChange(e.target.checked);
}
render() {
return (
<form>
<input
type="text"
placeholder="Search..."
value={this.props.filterText}
onChange={this.handleFilterTextChange}
/>
<p>
<input
type="checkbox"
checked={this.props.inStockOnly}
onChange={this.handleInStockChange}
/>{" "}
Only show products in stock
</p>
</form>
);
}
}
class FilterableProductTable extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: "",
inStockOnly: false
};
this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
this.handleInStockChange = this.handleInStockChange.bind(this);
}
handleFilterTextChange(filterText) {
this.setState({
filterText: filterText
});
}
handleInStockChange(inStockOnly) {
this.setState({
inStockOnly: inStockOnly
});
}
render() {
return (
<div>
<SearchBar
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
onFilterTextChange={this.handleFilterTextChange}
onInStockChange={this.handleInStockChange}
/>
<ProductTable
products={this.props.products}
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
/>
</div>
);
}
}
const PRODUCTS = [
{
category: "Sporting Goods",
price: "$49.99",
stocked: true,
name: "Football"
},
{
category: "Sporting Goods",
price: "$9.99",
stocked: true,
name: "Baseball"
},
{
category: "Sporting Goods",
price: "$29.99",
stocked: false,
name: "Basketball"
},
{
category: "Electronics",
price: "$99.99",
stocked: true,
name: "iPod Touch"
},
{
category: "Electronics",
price: "$399.99",
stocked: false,
name: "iPhone 5"
},
{ category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7" }
];
ReactDOM.render(
<FilterableProductTable products={PRODUCTS} />,
document.getElementById("root")
);